#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
#endregion

namespace GameFramework.AudioSystem
{
    /// <summary>
    /// General volume settings that cover a very basic range of volume values.
    /// Note that volume of an audio category is from 0.0 through 1.0 . The
    /// values listed here will be divided by 100 to obtain the correct volume.
    /// </summary>
    [Flags]
    public enum Volume : int
    {
        Mute            = 0,
        OneThird        = 33,
        Half            = 50,
        TwoThirds       = 66,  
        Full            = 100,
    }

    /// <summary>
    /// An audio management component that is used to simplfy the use of audio
    /// playback in a XNA GameStudioExpress game or application project.
    /// </summary>
    public class AudioManager : Microsoft.Xna.Framework.GameComponent
    {
        #region Fields
        
        /// <summary>
        /// AudioEngine specific fields to keep track of the current AudioEngine
        /// settings file name, and provide the AudioEngine instance that will 
        /// be used to play back any audio cues through this manager.
        /// </summary>
        private string engineSettingsFile;
        private AudioEngine audioEngine;

        /// <summary>
        /// AudioEngine specific WaveBank fields to keep track of the current Audio-
        /// Engine compiled WaveBank and WaveBank filename.
        /// </summary>
        private string engineWaveBankFile;
        private WaveBank engineWaveBank;

        /// <summary>
        /// AudioEngine specific SoundBank fields to keep track of the current Audio-
        /// Engine compiled SoundBank and SoundBank filename.
        /// </summary>
        private string engineSoundBankFile;
        private SoundBank engineSoundBank;

        /// <summary>
        /// Track the over-all status of the AudioManager class to make sure that 
        /// there is at least valid AudioEngine, WaveBank and SoundBank instances
        /// exist.
        /// </summary>
        private bool isManagerReady = false;

        /// <summary>
        /// Storage for the loaded audio cues and categories.
        /// </summary>
        private Dictionary<string, Cue> engineCues;
        private Dictionary<string, CategoryObject> engineCategories;

        #endregion

        #region Properties

        /// <summary>
        /// Indexer[Get] the cue instance from the AudioManager storage
        /// </summary>
        /// <param name="cueNameToFind">XACT Cue name to reference from AudioManager storage.</param>
        /// <returns>(Cue) Cue instance requested, or null if not found or error.</returns>
        public Cue this[string cueNameToFind]
        {
            get
            {
                if (engineCues.ContainsKey(cueNameToFind))
                    return engineCues[cueNameToFind];
                else
                    return null;
            }
        }

        /// <summary>
        /// Get the current state of the AudioManager.
        /// </summary>
        public bool IsReady
        {
            get { return isManagerReady; }
        }

        /// <summary>
        /// Get the current AudioEngine instance for the AudioManager.
        /// </summary>
        public AudioEngine Engine
        {
            get { return audioEngine; }
        }

        /// <summary>
        /// Get the current AudioEngine instance main audio WaveBank.
        /// </summary>
        public WaveBank Waves
        {
            get { return engineWaveBank; }
        }

        /// <summary>
        /// Get the current AudioEngine instance main audio SoundBank.
        /// </summary>
        public SoundBank Sounds
        {
            get { return engineSoundBank; }
        }

        /// <summary>
        /// Get the current AudioEngine instance main audio cue storage.
        /// </summary>
        public Dictionary<string, Cue> Cues
        {
            get { return engineCues; }
        }

        /// <summary>
        /// Get the current AudioEngine instance main audio category storage.
        /// </summary>
        public Dictionary<string, CategoryObject> Categories
        {
            get { return engineCategories; }
        }

        #endregion

        #region Construction

        /// <summary>
        /// Construct an AudioManager instance using the specified parameters.
        /// </summary>
        /// <param name="xactEngineSettingsFile">XACT Compiled settings filename (Requirs extention).</param>
        /// <param name="xactWaveBankName">XACT Compiled WaveBank filename (Requires extention)</param>
        /// <param name="xactSoundBankName">XACT Compile SoundBank filename (Required extention)</param>
        /// <param name="game">XNA GameStudioExpress game instance for this application</param>
        public AudioManager(string xactEngineSettingsFile, string xactWaveBankName, string xactSoundBankName, Game game)
            : base(game)
        {
            // Ensure that none of the filename values are empty or null, and if they are
            // they throw the appropriate exception to let the calling code know there was
            // an error.
            if (String.IsNullOrEmpty(xactEngineSettingsFile))
                throw new ArgumentNullException("xactEngineSettingsFile");

            engineSettingsFile = xactEngineSettingsFile;

            if( String.IsNullOrEmpty(xactWaveBankName) )
                throw new ArgumentNullException("xactWaveBankName");

            engineWaveBankFile = xactWaveBankName;

            if( String.IsNullOrEmpty(xactSoundBankName) )
                 throw new ArgumentNullException("xactSoundBankName");

            engineSoundBankFile = xactSoundBankName;

            // Initialize the audio cue storage and audio category fields.
            engineCues = new Dictionary<string, Cue>();
            engineCategories = new Dictionary<string, CategoryObject>();
        }

        #endregion

        #region Methods

        #region Methods - Category Methods

        /// <summary>
        /// Adds a new category to the AudioManager
        /// </summary>
        /// <param name="categoryToLoad">XACT Category name to load into the AudioManager.</param>
        /// <returns>(bool) Success (true) if the category was added, otherwise returns (false). </returns>
        public virtual bool AddCategory(string categoryToLoad)
        {
            // Check to ensure that the category name to load isn't empty or null.
            if (String.IsNullOrEmpty(categoryToLoad))
                return false;

            // Create and add the new category to the AudioManager category storage by the name 
            // specified. Note that this will over-write any category instance that is currently
            // loaded or stored.
            engineCategories[categoryToLoad] = new CategoryObject(categoryToLoad, audioEngine); // audioEngine.GetCategory(categoryToLoad);

            return true;
        }

        /// <summary>
        /// Adds new categories to the AudioManager by using an array of category names to load.
        /// </summary>
        /// <param name="categoriesToLoad">XACT Category array of category names to load into the AudioManager.</param>
        /// <returns>
        /// (int) Category count of all successfully added categories.
        /// </returns>
        public virtual int AddCategories(string[] categoriesToLoad)
        {
            // Track the successfully added category count.
            int categoryCount = 0;

            // Loop through each string in the array of category names and process
            foreach (string workCategory in categoriesToLoad)
            {
                //  If one of the array elements is empty or null then skip it and 
                // fetch the next category name to load.
                if (String.IsNullOrEmpty(workCategory))
                    continue;

                // If the category was added successfully then incremement the category count.
                if (this.AddCategory(workCategory))
                    categoryCount += 1;
            }

            return categoryCount;
        }

        /// <summary>
        /// Delete the indicated category name from the AudioManager's  category storage.
        /// </summary>
        /// <param name="categoryToDelete">XACT Category name to remove from the AudioManager.</param>
        /// <returns>(bool) Success (true) if the category was removed, otherwise returns (false). </returns>
        public virtual bool DeleteCategory(string categoryToDelete)
        {
            // Ensure that the category name to remove isn't empty or null at the same
            // time making sure that category actually exists and isn't one of the two
            // default XACT sound categories "Default" and "Music".
            if (String.IsNullOrEmpty(categoryToDelete) ||
                !engineCategories.ContainsKey(categoryToDelete)||
                categoryToDelete.ToUpper() == "DEFAULT" ||
                categoryToDelete.ToUpper() == "MUSIC" )
                return false;

            // Remove the category object from the AudioManager storage.
            engineCategories.Remove(categoryToDelete);

            return true;
        }

        /// <summary>
        /// Look up the specified category name in the AudioManager storage.
        /// </summary>
        /// <param name="categoryName">Category name to search</param>
        /// <returns>
        /// (AudioCategory) Category instance of the specified category name. Throws ArgumentException.
        /// </returns>
        public virtual AudioCategory Category(string categoryName)
        {
            // Ensure that the specified category name isn't empty or null and 
            // that the category name actually exists. If not throw an exception
            // to let the calling code know there was a problem.
            if (String.IsNullOrEmpty(categoryName) ||
                !engineCategories.ContainsKey(categoryName))
                throw new ArgumentException("categoryName");

            // Return the actual category instance.
            return engineCategories[categoryName].Category;
        }

        #endregion

        #region Methods - Cue Methods

        /// <summary>
        /// Adds or refreshes a new cue instance and stores it bacl in the AudioManager storage.
        /// </summary>
        /// <param name="cueNameToLoad">XACT Cue name to load into the AudioManager.</param>
        /// <returns>(bool) Success (true) if the cue was added, otherwise returns (false).</returns>
        public virtual bool Add(string cueNameToLoad)
        {
            // Ensure that the specified cue name to load isn't empty or null
            if (String.IsNullOrEmpty(cueNameToLoad))
                return false;

            // Add or refresh the specified cue name in the the AudioManager cue storage
            engineCues[ cueNameToLoad ] = engineSoundBank.GetCue(cueNameToLoad);

            return true;
        }

        /// <summary>
        /// Adds new cues to the AudioManager by using an array of cue names to load.
        /// </summary>
        /// <param name="cuesToLoad">XACT Cue array of names to load into the AudioManager.</param>
        /// <returns>(int) Cue count of all successfully added cues.</returns>
        public virtual int Add(string[] cuesToLoad)
        {
            // Track the successfull addition of audio cues 
            int cueCount = 0;

            // Loop through each of the cue names in the array and process
            foreach (string cueName in cuesToLoad)
            {
                // Ensure that any of the cue names that are empty or null are skipped
                if (String.IsNullOrEmpty(cueName))
                    continue;

                // Add the cue to the AudioManager storage and increment the successfully
                // cue count field.
                if (this.Add(cueName))
                    cueCount += 1;
            }

            return cueCount;
        }

        /// <summary>
        /// Removed a cue from the AudioManager storage.
        /// </summary>
        /// <param name="cueNameToDelete">XACT Cue name to remove from the AudioManager.</param>
        /// <returns>(bool) Upon successfull removal returns (true), otherwise returns (false).</returns>
        public virtual bool RemoveCue(string cueNameToDelete)
        {
            // Ensure that the specified cue name isn't empty or null, and the
            // requested cue name actually is stored in the AudioManager storage.
            if (String.IsNullOrEmpty(cueNameToDelete) ||
                !engineCues.ContainsKey( cueNameToDelete ) )
                return false;

            // Actually remove the specified cue name from the AudioManager storage.
            engineCues.Remove(cueNameToDelete);

            return true;
        }

        #endregion

        #region Methods - Audio Manipulation

        /// <summary>
        /// Play an audio cue without storing, or keeping track of the related cue instance.
        /// NOTE that all audio cues that have been associated with an XACT category are 
        /// still effected by any volume or pitch value changes performed on the respected
        /// audio category.
        /// </summary>
        /// <param name="cueNameToPlay">XACT Cue name to play</param>
        /// <returns>(bool) Returns true if the requested cue hsa been played, otherwise returns null.</returns>
        public bool PlayCue(string cueNameToPlay)
        {
            // Ensure that the specified cue name to play isn't empty or null.
            if (String.IsNullOrEmpty(cueNameToPlay))
                return false;

            // Use the PlayCue method of the AudioManager SoundBank to play the requested sound
            engineSoundBank.PlayCue(cueNameToPlay);

            return true;
        }

        /// <summary>
        /// Play or load the specified cue name and manage it's state.
        /// </summary>
        /// <param name="cueNameToPlay">XACT Cue name to play</param>
        /// <returns>(Cue) Return the cue instance up success, otherwise return null.</returns>
        public Cue Play(string cueNameToPlay)
        {
            // Ensure that the specified cue name to play isn't empty or null, and
            // the requested cue name is actually stored in the AudioManager storage.
            if (String.IsNullOrEmpty(cueNameToPlay) ||
                !engineCues.ContainsKey(cueNameToPlay) )
                return null;

            // Need to do some checking to see if a cue has stopped. If so then the cue instance
            // needs to be refreshed before it can be used again.
            if (GameManager.AudioManager[cueNameToPlay].IsStopped)
            {
                // Add or Refresh the specified cue name
                GameManager.AudioManager.Add(cueNameToPlay);

                // Play the specified cue name.
                GameManager.AudioManager[cueNameToPlay].Play();
            }
            else if (GameManager.AudioManager[cueNameToPlay].IsPrepared)
            {
                // The cue instance appears to be valid and can be used, so just play
                // the specified cue name.
                GameManager.AudioManager[cueNameToPlay].Play();
            }

            // Return the cue instance to the calling code.
            return GameManager.AudioManager[cueNameToPlay];
        }

        /// <summary>
        /// Stop the specified cue name from playing.
        /// </summary>
        /// <param name="cueNameToStop">XACT Cue name to be stopped.</param>
        /// <returns>(bool) Return true is the cue was successfully stopped, otherwise return false.</returns>
        public bool Stop(string cueNameToStop)
        {
            // Ensure that the specified cue name to stop isn't empty or null, and 
            // the requested cue is actually stored in the AudioManager storage.
            if (String.IsNullOrEmpty(cueNameToStop) ||
                !engineCues.ContainsKey(cueNameToStop))
                return false;

            // Ensure that we only stope an audio cue that is playing, or stopping.
            if (engineCues[cueNameToStop].IsPlaying &&
                !engineCues[cueNameToStop].IsStopping)
                engineCues[cueNameToStop].Stop(AudioStopOptions.Immediate);

            return true;
        }

        /// <summary>
        /// Stop all currently playing audio sounds that are playing.
        /// </summary>
        public void StopAllPlaying()
        {
            // Loop through each stored cue looking for the ones that are currently playing.
            foreach (KeyValuePair<string, Cue> workPair in this.engineCues)
            {
                // Ensure that the cue is actually playing or there is no reason to stop it.
                if(workPair.Value.IsPlaying)
                    workPair.Value.Stop(AudioStopOptions.AsAuthored);
            }
        }

        /// <summary>
        /// Pause the play-back of the specified cue name.
        /// </summary>
        /// <param name="cueNameToPause">XACT Cue name to pause</param>
        /// <returns>(bool) Returns true if the cue was paused successfully, otherwise returns false.</returns>
        public bool Pause(string cueNameToPause)
        {
            // Ensure that the cue name specified isn't empty or null, and the
            // requested cue name actually exists in the AudioManager storage.
            if (String.IsNullOrEmpty(cueNameToPause) ||
                !engineCues.ContainsKey(cueNameToPause))
                return false;

            // Can only pause a currently playing cue.
            if (!engineCues[cueNameToPause].IsPlaying)
                return false;

            // Pause the specified cue.
            engineCues[cueNameToPause].Pause();

            return true;
        }

        /// <summary>
        /// Resume the play-back on the specified cue that has been previously paused.
        /// </summary>
        /// <param name="cueNameToResume">XACT Cue name to resume play-back</param>
        /// <returns>
        /// (bool) Returns true if the audio cue was successfully resumed, otherwise returns false;
        /// </returns>
        public bool Resume(string cueNameToResume)
        {
            // Ensure that the specified cue name isn't empty or null, and that it
            // currently exists in the AudioManager storage.
            if (String.IsNullOrEmpty(cueNameToResume) ||
                !engineCues.ContainsKey(cueNameToResume))
                return false;

            // Get a working cue to use to check current sound rendering state.
            Cue workCue = engineCues[cueNameToResume];

            // Ensure that only a paused cue is resumed.
            if (workCue.IsPaused)
                workCue.Resume();

            return true;
        }

        #endregion

        #region Methods - Component Required

        /// <summary>
        /// Initialize the AudioManager component and perform any initialization that needs
        /// to be performed at the point the AudioManager component is created.
        /// </summary>
        public override void Initialize()
        {
            // Create and initialize the AudioEngine with the XACT compiled settings file.
            audioEngine = new AudioEngine(engineSettingsFile);

            // Create an initialize the AudioEngine with the XACT compiled WaveBank, and 
            // SoundBank files.
            engineWaveBank = new WaveBank(audioEngine, engineWaveBankFile);
            engineSoundBank = new SoundBank(audioEngine, engineSoundBankFile);

            // Since XACT will not allow the deletion of the Music, or the Default categories
            // might as well add the categories to the AudioManager storage. 
            Categories["Default"] = new CategoryObject("Default", this.audioEngine);
            Categories["Music"] = new CategoryObject("Music", this.audioEngine);

            // Idicate that the AudioManager has been initialized and is ready to be used.
            isManagerReady = true;

            base.Initialize();
        }

        /// <summary>
        /// Refresh a loaded or stored cue instance.
        /// </summary>
        /// <param name="cueNameToRefresh">XACT Cue name to refresh.</param>
        public virtual void RefreshCue(string cueNameToRefresh)
        {
            // Ensure that the cue name to refresh isn't empty or null and
            // that the cue being refreshed is actually stored in the AudioManager
            // storage.
            if (String.IsNullOrEmpty(cueNameToRefresh) ||
                !engineCues.ContainsKey(cueNameToRefresh))
                return;

            // Get a working copy of the cue that is to be refreshed.
            Cue workCue = engineCues[cueNameToRefresh];

            // Ensure that the specified cue isn't in the process of playing, or 
            // has not already been prepared, or in the process of stopping.
            if (workCue.IsPrepared || workCue.IsPlaying || workCue.IsPaused
                || workCue.IsPreparing || workCue.IsStopping)
                return;
            
            // Refresh the cue, and update the AudioManager storage.
            workCue = engineSoundBank.GetCue(cueNameToRefresh);
            engineCues[cueNameToRefresh] = workCue;
        }

        /// <summary>
        /// Allows the AudioManager component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // Update this AudioEngine object each update cycle for proper operation.
            this.audioEngine.Update();

            base.Update(gameTime);
        }

        /// <summary>
        /// Dispose of any manually managed instances used in the operation of the AudioManager.
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            // Clean up any loaded cues, and call their dispose methods.
            foreach (KeyValuePair<string, Cue> workPair in engineCues)
            {
                workPair.Value.Stop(AudioStopOptions.Immediate);
                workPair.Value.Dispose();
            }

            this.engineSoundBank.Dispose();
            this.engineWaveBank.Dispose();
            this.audioEngine.Dispose();
            
            base.Dispose(disposing);
        }

        #endregion

        #endregion
    }
}


